﻿using System;
using System.Diagnostics;
using System.Threading;

namespace Ns.Threading
{
    /// <summary>
    /// Provides a mechanism for scheduling an <see cref="Action"/> 
    /// with a specified <see cref="Recurrence"/>
    /// as an alternative to the <see cref="System.Threading.Timer"/> which does not consider the <see cref="System.Globalization.DaylightTime"/>.
    /// This class cannot be inherited.
    /// </summary>
    public sealed class ScheduledTimer : IDisposable
    {
        private readonly TimeSpan Infinite = TimeSpan.FromMilliseconds(-1);
        private bool Disposed = false;

        /// <summary>
        /// Gets the recurrence of the scheduled <see cref="Action"/>.
        /// </summary>
        public Recurrence Recurrence { get; private set; }

        /// <summary>
        /// Gets the underlying timer.
        /// </summary>
        private Timer Timer { get; set; }

        /// <summary>
        /// Gets the scheduled action.
        /// </summary>
        public TimerCallback Action { get; private set; }

        /// <summary>
        /// Gets the number of scheduled occurrences or zero (0) for infinite schedule.
        /// </summary>
        public int Occurrences { get; private set; }

        /// <summary>
        /// Gets the schedule of the <see cref="NextOccurrence"/>.
        /// </summary>
        public OccurrenceSchedule NextSchedule { get; private set; }

        /// <summary>
        /// Gets the next occurrence (one-based) or zero (0) in case of infinite schedule.
        /// </summary>
        public int NextOccurrence { get; private set; }
       
        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return (Action != null && Recurrence != null)
                ? string.Format("{0} {1}"
                    , Action.Method.ToString() 
                    , Recurrence.ToString())
                : base.ToString();
        }
        
        #region Disposable Members

        /// <summary>
        /// Releases all resources used by the current instance of <see cref="ScheduledTimer"/>.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        private void Dispose(bool disposing)
        {
            if (!Disposed)
            {
                if (disposing)
                {
                    if (Timer != null)
                    {
                        Timer.Dispose();
                        Timer = null;
                    }
                }
                Disposed = true;
            }
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="ScheduledTimer"/> is reclaimed by garbage collection.
        /// </summary>
        ~ScheduledTimer()
        {
            Dispose(false);
        }
        
        #endregion

        #region Constructors

        private ScheduledTimer()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScheduledTimer"/> class
        /// starting immediately with an infinite schedule.
        /// </summary>
        /// <param name="action">An <see cref="Action"/> delegate representing a method to be executed.</param>
        /// <param name="recurrence">A recurrence instance.</param>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> or <paramref name="recurrence"/> is null.</exception>
        public ScheduledTimer(TimerCallback action, Recurrence recurrence)
            : this(action, null, recurrence, default(DateTime), 0)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScheduledTimer"/> class
        /// starting immediately with an infinite schedule.
        /// </summary>
        /// <param name="action">An <see cref="Action"/> delegate representing a method to be executed.</param>
        /// <param name="state">An object containing information to be used by the scheduled <paramref name="action"/>, or <code>null</code>.</param>
        /// <param name="recurrence">A recurrence instance.</param>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> or <paramref name="recurrence"/> is null.</exception>
        public ScheduledTimer(TimerCallback action, object state, Recurrence recurrence)
            : this(action, state, recurrence, default(DateTime), 0)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScheduledTimer"/> class
        /// starting on the specified <see cref="DateTime"/> with an infinite schedule.
        /// </summary>
        /// <param name="action">An <see cref="Action"/> delegate representing a method to be executed.</param>
        /// <param name="recurrence">A recurrence instance.</param>
        /// <param name="startingOn">The date and time of the first execution assumed to be an universal time if not specified to avoid ambiguous dates. 
        /// Specify <code>default(<see cref="DateTime"/>)</code> to start immediately.</param>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> or <paramref name="recurrence"/> is null.</exception>
        public ScheduledTimer(TimerCallback action, Recurrence recurrence, DateTime startingOn)
            : this(action, null, recurrence, startingOn, 0)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScheduledTimer"/> class
        /// starting on the specified <see cref="DateTime"/> with an infinite schedule.
        /// </summary>
        /// <param name="action">An <see cref="Action"/> delegate representing a method to be executed.</param>
        /// <param name="state">An object containing information to be used by the scheduled <paramref name="action"/>, or <code>null</code>.</param>
        /// <param name="recurrence">A recurrence instance.</param>
        /// <param name="startingOn">The date and time of the first execution assumed to be an universal time if not specified to avoid ambiguous dates. 
        /// Specify <code>default(<see cref="DateTime"/>)</code> to start immediately.</param>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> or <paramref name="recurrence"/> is null.</exception>
        public ScheduledTimer(TimerCallback action, object state, Recurrence recurrence, DateTime startingOn)
            : this(action, state, recurrence, startingOn, 0)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScheduledTimer"/> class
        /// starting on the specified <see cref="DateTime"/> for the specified scheduled <paramref name="occurrences"/>.
        /// </summary>
        /// <param name="action">An <see cref="Action"/> delegate representing a method to be executed.</param>
        /// <param name="recurrence">A recurrence instance.</param>
        /// <param name="startingOn">The date and time of the first execution assumed to be an universal time if not specified to avoid ambiguous dates. 
        /// Specify default(<see cref="DateTime"/>) to start immediately.</param>
        /// <param name="occurrences">The number of scheduled occurrences. Specify zero (0) for an infinite schedule.</param>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> or <paramref name="recurrence"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="occurrences"/> is negative.</exception>
        public ScheduledTimer(TimerCallback action, Recurrence recurrence, DateTime startingOn, int occurrences)
            : this(action, null, recurrence, startingOn, occurrences)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScheduledTimer"/> class
        /// starting on the specified <see cref="DateTime"/> for the specified scheduled <paramref name="occurrences"/>.
        /// </summary>
        /// <param name="action">An <see cref="Action"/> delegate representing a method to be executed.</param>
        /// <param name="state">An object containing information to be used by the scheduled <paramref name="action"/>, or <code>null</code>.</param>
        /// <param name="recurrence">A recurrence instance.</param>
        /// <param name="startingOn">The date and time of the first execution assumed to be an universal time if not specified to avoid ambiguous dates. 
        /// Specify default(<see cref="DateTime"/>) to start immediately.</param>
        /// <param name="occurrences">The number of scheduled occurrences. Specify zero (0) for an infinite schedule.</param>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> or <paramref name="recurrence"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="occurrences"/> is negative.</exception>
        public ScheduledTimer(TimerCallback action, object state, Recurrence recurrence, DateTime startingOn, int occurrences)
        {
            Change(action, state, recurrence, startingOn, occurrences);
        }
        #endregion

        #region Methods

        /// <summary>
        /// Changes the parameters of the <see cref="ScheduledTimer"/>.
        /// </summary>
        /// <param name="action">An <see cref="Action"/> delegate representing a method to be executed.</param>
        /// <param name="state">An object containing information to be used by the scheduled <paramref name="action"/>, or <code>null</code>.</param>
        /// <param name="recurrence">A recurrence instance.</param>
        /// <param name="startingOn">The date and time of the first execution assumed to be an universal time if not specified to avoid ambiguous dates. 
        /// Specify default(<see cref="DateTime"/>) to start immediately.</param>
        /// <param name="occurrences">The number of scheduled occurrences. Specify zero (0) for an infinite schedule.</param>
        /// <exception cref="ArgumentNullException"><paramref name="action"/> or <paramref name="recurrence"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="occurrences"/> is negative.</exception>
        public void Change(TimerCallback action, object state, Recurrence recurrence, DateTime startingOn, int occurrences)
        {
            if (action == null || recurrence == null)
                throw new ArgumentNullException(action == null ? "action" : "recurrence");

            if (occurrences < 0)
                throw new ArgumentOutOfRangeException("occurrences");

            if (Timer != null && !Disposed)
            {
                //invalid WaitHandle forces the operating system to wait for timer disposing.
                Timer.Dispose(new InvalidWaitHandle());
                Timer = null;
            }

            Action = action;
            Recurrence = recurrence;
            Occurrences = occurrences;

            NextOccurrence = (Occurrences != 0) ? 1 : 0;
            NextSchedule = (startingOn == default(DateTime))
                ? OccurrenceSchedule.Now
                : new OccurrenceSchedule(startingOn);

            Timer = new Timer(Callback, state, NextSchedule.DueTime, Infinite);
            Trace.TraceInformation("{0} of {1} {2}"
                , (Occurrences != 0)
                    ? string.Format("Occurrence {0}", NextOccurrence)
                    : "Next occurrence"
                , this.ToString()
                , NextSchedule.ToString());
        }

        private void Callback(object state)
        {
            try
            {
                //execute current occurrence
                var watcher = Stopwatch.StartNew();
                Action(state);
                Trace.TraceInformation("{0} of {1} {2} Completed in {3}"
                    , (Occurrences != 0)
                        ? string.Format("Occurrence {0}", NextOccurrence)
                        : "Occurrence"
                    , this.ToString()
                    , NextSchedule.ToString()
                    , watcher.Elapsed);

                //schedule next occurrence
                var nextOccurrence = (Occurrences != 0)
                    ? NextOccurrence + 1
                    : NextOccurrence;
                if (nextOccurrence > Occurrences)
                {
                    NextOccurrence = 0;
                    NextSchedule = default(OccurrenceSchedule);
                    if (Timer != null && !Disposed)
                    {
                        Timer.Change(Infinite, Infinite);
                        Trace.TraceInformation("No more occurrence to schedule on {0} after occurrence {1}"
                            , this.ToString()
                            , NextOccurrence);
                    }
                }
                else
                {
                    NextOccurrence = nextOccurrence;
                    NextSchedule = Recurrence.GetNextSchedule();
                    if (Timer != null && !Disposed)
                    {
                        Timer.Change(NextSchedule.DueTime, Infinite);
                        Trace.TraceInformation("{0} of {1} {2}"
                            , (Occurrences != 0)
                                ? string.Format("Occurrence {0}", NextOccurrence)
                                : "Next occurrence"
                            , this.ToString()
                            , NextSchedule.ToString());
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.TraceError("{0} of {1} {2} failed due to the error after:\r\n{3}"
                    , (Occurrences != 0)
                        ? string.Format("Occurrence {0}", NextOccurrence)
                        : "Occurrence"
                    , this.ToString()
                    , NextSchedule.ToString()
                    , ex.ToString());
            }
        }

        #endregion
    }
}